using System;
using System.Data;
using System.Diagnostics;
using System.IO;
using Agile.Common;
using Agile.Common.Reflections;

namespace Agile.Genie.Descriptors
{
    /// <summary>
    /// Summary description for VisualStudioProjectFile.
    /// </summary>
    public abstract class VisualStudioProjectFile
    {
        #region Constructors and Factories

        /// <summary>
        /// Construct with a project file.
        /// </summary>
        /// <param name="projectFile">The project file to instantiate with.</param>
        protected VisualStudioProjectFile(FileInfo projectFile)
        {
            if (!projectFile.Extension.EndsWith("proj"))
                throw new ApplicationException("File must be a visual studio project file.");
            _projectFile = AgileFileInfo.Build(projectFile);
        }

        #endregion

        private readonly AgileFileInfo _projectFile;

        private DataSet _projectFileData;

        /// <summary>
        /// Gets the project file.
        /// </summary>
        public AgileFileInfo ProjectFile
        {
            get { return _projectFile; }
        }

        /// <summary>
        /// Gets the project files data, loaded in a DataSet
        /// </summary>
        public DataSet ProjectFileData
        {
            get
            {
                if (_projectFileData == null)
                    _projectFileData = GetProjectFileData(ProjectFile.FileInfo);
                return _projectFileData;
            }
        }

        /// <summary>
        /// Gets the visual studio version the project file has been created for.
        /// </summary>
        public string VisualStudioVersion
        {
            get { return GetVisualStudioVersion(ProjectFileData); }
        }

        /// <summary>
        /// Gets the name of the project.
        /// </summary>
        public string ProjectName
        {
            get { return ProjectFile.NameExcludingExtension; }
        }

        /// <summary>
        /// Gets the name of the project file.
        /// </summary>
        public string ProjectFileName
        {
            get { return ProjectFile.Name; }
        }

        private static DirectoryInfo rootDevelopmentDirectory;
        /// <summary>
        /// Gets the root directory that all development projects branch from.
        /// </summary>
        protected DirectoryInfo RootDevelopmentDirectory
        {
            get 
            {
                if (rootDevelopmentDirectory == null)
                {
                    DirectoryInfo currentProjDir = _projectFile.FileInfo.Directory;
                    // just set the return value as exception raised if fail to find the root dir
                    rootDevelopmentDirectory = FindRootDevDirectory(currentProjDir);
                }
                return rootDevelopmentDirectory; 
            }
        }

        private static DirectoryInfo FindRootDevDirectory(DirectoryInfo dir)
        {
            // when we find the master.build file, we are at the root dev dir
            foreach (FileInfo file in dir.GetFiles())
            {
                if(file.Name.Equals("master.build", StringComparison.InvariantCultureIgnoreCase))
                    return dir;
            }
            // if it has a parent then try finding the master.build in that
            if (dir.Parent != null)
                return FindRootDevDirectory(dir.Parent);
            // otherwise we failed to find it so throw an exception
            throw new DirectoryNotFoundException("Failed to find a directory that contains a 'master.build' file");
        }

        /// <summary>
        /// Returns true if there is a .Tests.csproj (in the appropriate standard directory) for this project.
        /// </summary>
        public bool HasTestProject
        {
            get
            {
                return GetTestProjectFile() != null;
            }
        }

        private FileInfo testProjectFile;
        /// <summary>
        /// Find and return the test project file for this project (null if there isn't one)
        /// </summary>
        /// <returns></returns>
        public FileInfo GetTestProjectFile()
        {
            if (testProjectFile == null)
            {
                // First check this directory then all sub directories for the test project
                testProjectFile = GetTestProjectFile(_projectFile.FileInfo.Directory);
                if (testProjectFile != null) return testProjectFile;


                // Check all of the sub directories of this one
                testProjectFile = FindTestProjectInSubDirectories(_projectFile.FileInfo.Directory, "bin", "obj");
                if (testProjectFile != null) return testProjectFile;

                // finally try the parent and its sub dirs (exclude this dir)
                if (_projectFile.FileInfo.Directory != null)
                    testProjectFile = FindTestProjectInSubDirectories(_projectFile.FileInfo.Directory.Parent, "bin", "obj");
            }
            return testProjectFile;
        }


        /// <summary>
        /// Loop through all of the sub directories of the given directory.
        /// </summary>
        /// <param name="directory">Directory to search (including all sub dirs)</param>
        /// <param name="ignoreDirectoriesStartingWith">Ignore any directory (and its sub directories) that starts with any of the
        /// given strings. Use only for directories like '_svn' or where you know the name is unique.</param>
        public FileInfo FindTestProjectInSubDirectories(DirectoryInfo directory
            , params string[] ignoreDirectoriesStartingWith)
        {
            if (ignoreDirectoriesStartingWith != null)
            {
                foreach (string ignore in ignoreDirectoriesStartingWith)
                {
                    if (directory.Name.StartsWith(ignore))
                        return null;
                }
            }

            foreach (DirectoryInfo sub in directory.GetDirectories())
            {
                FileInfo file = FindTestProjectInSubDirectories(sub, ignoreDirectoriesStartingWith);
                if(file != null)
                    return file;
            }

            // search for the file in the dir.
            return GetTestProjectFile(directory);
        }
        /// <summary>
        /// returns the test project file or null if not found
        /// </summary>
        private FileInfo GetTestProjectFile(DirectoryInfo directory)
        {
            foreach (FileInfo file in directory.GetFiles())
            {
                if (file.Name.StartsWith(ProjectName, StringComparison.InvariantCultureIgnoreCase))
                {
                    // it must also have Test in the name
                    if (file.Name.Contains("Test"))
                    {
                        // and finally it also needs to be a project file
                        if (file.Name.EndsWith(".csproj") || file.Name.EndsWith(".vbproj"))
                            return file;
                    }
                }
            }
            return null;
        }

        #region Abstract Methods

        /// <summary>
        /// Gets the projects output type, e.g. Exe or library
        /// </summary>
        public abstract string OutputType { get; }


        /// <summary>
        /// Gets the type of project, e.g. PocketPC
        /// </summary>
        public abstract string PlatformFamilyName { get; }

        /// <summary>
        /// Returns true if the project SHOULD have unit tests
        /// </summary>
        public bool IsUnitTested
        {
            get
            {
                // TODO: add these to the config or generator data.
                if (ProjectFileName.Contains("Shared.Resources"))
                    return false;
                if (ProjectFileName.Contains("Shared.CABExtensions"))
                    return false;
                return !IsCompactFramework;
            }
        }

        /// <summary>
        /// Returns true if the project is for the compact framework.
        /// </summary>
        public bool IsCompactFramework
        {
            get
            {
                try
                {
                    if (PlatformFamilyName.ToLower() == "pocketpc")
                        return true;
                }
                catch (Exception ex)
                {
                    Debug.WriteLine(ex.Message);
                    return false;
                }
                return false;
            }
        }


        /// <summary>
        /// Gets the datatable from the dataset that relate specifically to references to 
        /// other projects.
        /// </summary>
        protected abstract DataTable ProjectReferenceRows { get; }

        /// <summary>
        /// Gets the path (including the application name) of the visual studio
        /// version that needs to be used to open the project file.
        /// </summary>
        public abstract string VisualStudioApplicationPath { get; }

        /// <summary>
        /// Gets the Assembly name of the project.
        /// </summary>
        public abstract string ProjectAssemblyName { get; }

        /// <summary>
        /// Gets the projects GUID.
        /// </summary>
        public abstract Guid ProjectGuid { get; }

        /// <summary>
        /// Gets the Assembly name from the given Reference table row.
        /// </summary>
        /// <param name="referenceTableRow">A row from the Reference table.</param>
        /// <returns></returns>
        public abstract string GetReferencedAssemblyName(DataRow referenceTableRow);

        #endregion

        #region Static Methods

        /// <summary>
        /// Gets the xml project file from the project file,
        /// returned in a dataset.
        /// </summary>
        /// <param name="projectFile">Project file to get the xml data from.</param>
        /// <returns>DataSet containing project file data.</returns>
        private static DataSet GetProjectFileData(FileInfo projectFile)
        {
            var projectFileData = new DataSet();
            projectFileData.ReadXml(projectFile.FullName);
            return projectFileData;
        }

        /// <summary>
        /// Gets the .net version the project has been created for.
        /// </summary>
        /// <param name="projectFileData">DataSet containing the project files data.</param>
        /// <returns></returns>
        private static string GetVisualStudioVersion(DataSet projectFileData)
        {
            // Bit of a dodgy way of getting the version....but it works!
            if (projectFileData.DataSetName == "VisualStudioProject")
                return "2003";
            if ((projectFileData.DataSetName == "NewDataSet") && (projectFileData.Tables[0].TableName == "Project"))
                return "2005";
            throw new AgileCommonException("Unknown project target framework type. Error in GetVisualStudioVersion().");
        }

        /// <summary>
        /// Gets the version for the given project file
        /// </summary>
        /// <param name="projectFile">Project file we need to determine the version of.</param>
        /// <returns>2003 or 2005 (at the moment)</returns>
        public static string GetVersion(FileInfo projectFile)
        {
            DataSet data = GetProjectFileData(projectFile);
            return GetVisualStudioVersion(data);
        }

        #endregion

        #region Referenced Assemblies

        private string[] _referencedAssemblies;

        private VisualStudioProjectFileCollection _referencesToOurAssemblies;

        /// <summary>
        /// Gets ALL assemblies this project references.
        /// </summary>
        public string[] ReferencedAssemblies
        {
            get
            {
                if (_referencedAssemblies == null)
                    LoadAllReferencedAssemblies();
                return _referencedAssemblies;
            }
        }

        /// <summary>
        /// Gets the projects that this project reference. Either through referencing
        /// the dll or the actual project file (only OUR projects).
        /// </summary>
        /// <remarks>The project may reference a dll or it may reference the
        /// specific project.</remarks>
        public VisualStudioProjectFileCollection ReferencedProjects
        {
            get
            {
                if (_referencesToOurAssemblies == null)
                {
                    _referencesToOurAssemblies = VisualStudioProjectFileCollection.Build();
                    _referencesToOurAssemblies.AddRange(LoadDllReferencesToOurAssemblies());
                    _referencesToOurAssemblies.AddRange(LoadProjectReferencesToOurAssemblies());
                }
                return _referencesToOurAssemblies;
            }
        }

        /// <summary>
        /// Loads the _referencedAssemblies string array with 
        /// all of the assemblies that the project references.
        /// </summary>
        private VisualStudioProjectFileCollection LoadDllReferencesToOurAssemblies()
        {
            VisualStudioProjectFileCollection items = VisualStudioProjectFileCollection.Build();
            foreach (DataRow row in ProjectFileData.Tables["Reference"].Rows)
            {
                string assemblyName = GetReferencedAssemblyName(row);

                // If its not one of ours we wont have the project file...so don't look for it.
                if (!Assemblies.IsOneOfOurs(assemblyName))
                    continue;

                Debug.WriteLine(string.Format("{0} loading DLL reference to {1}", DateTime.Now.ToString("mm:ss:fff"), assemblyName));
                VisualStudioProjectFile file = VisualStudioProjectFileFactory.InstantiateFromDllReference(assemblyName
                                                                                                          , RootDevelopmentDirectory);
                if (file == null)
                    continue;

                try
                {
                    items.Add(file); // exception gets thrown if project file cannot be found.
                }
                catch (Exception ex)
                {
                    if (ex is ArgumentNullException)
                    {
                        //TODO: Log the fact that the project file could not be found
                        Debug.Write(string.Format("Failed to create VisualStudioProjectFile for dll '{0}' because could not find a project file."
                            , assemblyName));
                    }
                    else
                        throw;
                }
            }
            return items;
        }

        /// <summary>
        /// Loads the set of references to our assemblies.
        /// i.e the projects this project directly references.
        /// </summary>
        private VisualStudioProjectFileCollection LoadProjectReferencesToOurAssemblies()
        {
            VisualStudioProjectFileCollection items = VisualStudioProjectFileCollection.Build();
            foreach (DataRow row in ProjectReferenceRows.Rows)
            {
                try
                {
                    string project = row["Project"].ToString();
                    Debug.WriteLine(string.Format("{0} loading Project reference to {1}", DateTime.Now.ToString("mm:ss:fff"), project));
                    VisualStudioProjectFile file = VisualStudioProjectFileFactory.InstantiateFromProjectReference(new Guid(project)
                                                                                       , RootDevelopmentDirectory);
                    if (file == null)
                        continue;
                    items.Add(file);
                }
                catch (Exception ex) // exception gets thrown if project file cannot be found.
                {
                    Debug.WriteLine(ex.Message);
                }
            }
            return items;
        }

        /// <summary>
        /// Loads the _referencedAssemblies string array with 
        /// all of the assemblies that the project references.
        /// </summary>
        private void LoadAllReferencedAssemblies()
        {
            DataTable referenceTable = ProjectFileData.Tables["Reference"];
            int totalReferences = referenceTable.Rows.Count;
            _referencedAssemblies = new string[totalReferences];

            for (int i = 0; i < totalReferences; i++)
            {
                string referencedAssembly = GetReferencedAssemblyName(referenceTable.Rows[i]);
                _referencedAssemblies[i] = referencedAssembly;
            }
        }

        #endregion

    }
}